/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.compose.ui

import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.WindowPosition
import java.awt.*
import java.awt.event.InputMethodEvent
import java.awt.event.KeyEvent
import java.awt.event.KeyEvent.KEY_PRESSED
import java.awt.event.KeyEvent.KEY_RELEASED
import java.awt.event.MouseEvent
import java.awt.event.MouseWheelEvent
import java.awt.font.TextHitInfo
import java.awt.im.InputMethodRequests
import java.awt.image.BufferedImage
import java.awt.image.MultiResolutionImage
import java.text.AttributedString
import javax.swing.Icon
import javax.swing.ImageIcon
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.ExperimentalTime
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
import org.jetbrains.skiko.MainUIDispatcher

fun testImage(color: Color): Painter = run {
    val bitmap = ImageBitmap(100, 100)
    val paint = Paint().apply { this.color = color }
    Canvas(bitmap).drawRect(0f, 0f, 100f, 100f, paint)
    BitmapPainter(bitmap)
}

fun Image.readFirstPixel(): Color {
    val image = (this as MultiResolutionImage).getResolutionVariant(1.0, 1.0) as BufferedImage
    return Color(image.getRGB(0, 0))
}

fun Icon.readFirstPixel() = (this as ImageIcon).image.readFirstPixel()

private val os = System.getProperty("os.name").lowercase()
internal val isLinux = os.startsWith("linux")
internal val isWindows = os.startsWith("win")
internal val isMacOs = os.startsWith("mac")

fun Window.sendKeyEvent(
    code: Int,
    char: Char = code.toChar(),
    id: Int = KeyEvent.KEY_PRESSED,
    location: Int =
        if (id == KeyEvent.KEY_TYPED)
            KeyEvent.KEY_LOCATION_UNKNOWN
        else
            KeyEvent.KEY_LOCATION_STANDARD,
    modifiers: Int = 0
): Boolean {
    val event = KeyEvent(
        // if we would just use `focusOwner` then it will be null if the window is minimized
        mostRecentFocusOwner,
        id,
        0,
        modifiers,
        code,
        char,
        location
    )
    mostRecentFocusOwner!!.dispatchEvent(event)
    return event.isConsumed
}

fun Window.focusedInputMethodRequests(): InputMethodRequests? =
    mostRecentFocusOwner!!.inputMethodRequests

fun Window.sendKeyTypedEvent(
    char: Char,
    modifiers: Int = 0
) = sendKeyEvent(
    code = 0,
    char = char,
    id = KeyEvent.KEY_TYPED,
    modifiers = modifiers
)

fun Window.sendCharTypedEvents(char: Char) {
    sendKeyEvent(char.code, char, KEY_PRESSED)
    sendKeyTypedEvent(char)
    sendKeyEvent(char.code, char, KEY_RELEASED)
}

fun Window.sendInputMethodEvent(
    text: String?,
    committedCharacterCount: Int,
): Boolean {
    val event = InputMethodEvent(
        // if we would just use `focusOwner` then it will be null if the window is minimized
        mostRecentFocusOwner,
        InputMethodEvent.INPUT_METHOD_TEXT_CHANGED,
        0,
        if (text == null) null else AttributedString(text).iterator,
        committedCharacterCount,
        TextHitInfo.leading(0),
        TextHitInfo.leading(0)
    )
    mostRecentFocusOwner!!.dispatchEvent(event)
    return event.isConsumed
}

fun Container.sendMousePress(
    button: Int = MouseEvent.BUTTON1,
    x: Int,
    y: Int
): Boolean {
    val modifiers = when (button) {
        MouseEvent.BUTTON1 -> MouseEvent.BUTTON1_DOWN_MASK
        MouseEvent.BUTTON2 -> MouseEvent.BUTTON2_DOWN_MASK
        MouseEvent.BUTTON3 -> MouseEvent.BUTTON3_DOWN_MASK
        else -> 0
    }
    return sendMouseEvent(
        id = MouseEvent.MOUSE_PRESSED,
        x = x,
        y = y,
        modifiers = modifiers,
        button = button
    )
}

fun Container.sendMouseRelease(
    button: Int = MouseEvent.BUTTON1,
    x: Int,
    y: Int,
): Boolean {
    return sendMouseEvent(
        id = MouseEvent.MOUSE_RELEASED,
        x = x,
        y = y,
        modifiers = 0,
        button = button
    )
}

fun Container.sendMouseEvent(
    id: Int,
    x: Int,
    y: Int,
    modifiers: Int = 0,
    button: Int = MouseEvent.NOBUTTON,
): Boolean {
    // we use width and height instead of x and y because we can send (-1, -1), but still need
    // the component inside window
    val component = findComponentAt(width / 2, height / 2)
    val event = MouseEvent(
        component,
        id,
        0,
        modifiers,
        x,
        y,
        1,
        false,
        button
    )
    component.dispatchEvent(event)
    return event.isConsumed
}

fun Container.sendMouseWheelEvent(
    x: Int = width / 2,
    y: Int = height / 2,
    scrollType: Int = MouseWheelEvent.WHEEL_UNIT_SCROLL,
    wheelRotation: Double = 0.0,
    modifiers: Int = 0,
): Boolean {
    // we use width and height instead of x and y because we can send (-1, -1), but still need
    // the component inside window
    val component = findComponentAt(width / 2, height / 2)
    val event = MouseWheelEvent(
        component,
        MouseWheelEvent.MOUSE_WHEEL,
        0,
        modifiers,
        x,
        y,
        x,
        y,
        1,
        false,
        scrollType,
        1,
        wheelRotation.toInt(),
        wheelRotation
    )
    component.dispatchEvent(event)
    return event.isConsumed
}

private val EventComponent = object : Component() {}

internal fun awtWheelEvent(isScrollByPages: Boolean = false) = MouseWheelEvent(
    EventComponent,
    MouseWheelEvent.MOUSE_WHEEL,
    0,
    0,
    0,
    0,
    0,
    false,
    if (isScrollByPages) {
        MouseWheelEvent.WHEEL_BLOCK_SCROLL
    } else {
        MouseWheelEvent.WHEEL_UNIT_SCROLL
    },
    1,
    0
)

fun Component.performClick() {
    dispatchEvent(MouseEvent(this, MouseEvent.MOUSE_PRESSED, 0, 0,1, 1, 1, false, MouseEvent.BUTTON1))
    dispatchEvent(MouseEvent(this, MouseEvent.MOUSE_RELEASED, 0, 0,1, 1, 1, false, MouseEvent.BUTTON1))
}

// TODO(demin): It seems this not-so-good synchronization
//  doesn't cause flakiness in our window tests.
//  But more robust solution will be to using something like UiUtil
/**
 * Wait until all scheduled tasks in Event Dispatch Thread are performed.
 * New scheduled tasks in these tasks also will be performed
 */
suspend fun awaitEDT() {
    // Most of the work usually is done after the first yield(), almost all the work -
    // after fourth yield()
    repeat(100) {
        yield()
    }
}

fun Dimension.toDpSize() = DpSize(width.dp, height.dp)

fun Point.toWindowPosition() = WindowPosition(x.dp, y.dp)

fun Size.toInt() = IntSize(width.toInt(), height.toInt())

// to avoid races between GlobalSnapshotManager and scene.render, we need to run test in UI thread
//
// It is a bug of ImageComposeScene,
// calling scene.render should apply or await all global snapshot changes
// instead, it can skip sometimes applying changes,
// because GlobalSnapshotManager is currently applying them
// this results that scene.render won't recompose anything, event if there are states changed
internal inline fun <R> ImageComposeScene.useInUiThread(
    crossinline block: (ImageComposeScene) -> R
): R = runBlocking(MainUIDispatcher) {
    use(block)
}

@OptIn(ExperimentalTime::class)
internal fun ImageComposeScene.runUntilIdle(
    initialTime: Duration = Duration.ZERO,
    frameDuration: Duration = 16.milliseconds
): Duration {
    var time = initialTime
    render(time)
    while (hasInvalidations()) {
        time += frameDuration
        render(time)
    }
    return time
}
